﻿using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace WaferService
{
    public class WaferTransferService(IEFEMService efemService,
   IPLCService plcService,
   IMotionControlService motionControlService,
   IHVServiceClient hvClient,
   IEChuckServiceClient eChuckServiceClient,
   ILogger<WaferTransferService> logger,
   ILoggerFactory loggerFactory,
   IDbContextFactory<ApplicationDbContext> dbFactory,
   SubstrateTrackingService substrateTrackingService,
   LoadingPositionShim shim,
   IOptions<EFEMConfig> efemOptions)
    {
        const string WafersAtLargeMessage = "Some wafers are still at large. Substrates tracking may be out of sync";

        readonly object lockObj = new();

        WaferLoadingQueue? loadingQueue;

        int busy;

        public async Task Reset()
        {
            if (Interlocked.CompareExchange(ref busy, 0, 0) != 0)
            {
                throw new BadHttpRequestException("Another transfer is in progress.");
            }

            await Unlock();
        }

        public async Task<double> GetAlignerAngle()
        {
            using var ctx = dbFactory.CreateDbContext();
            return await GetAlignerAngle(ctx);
        }

        public async Task SetAlignerAngle(double angle)
        {
            using var ctx = dbFactory.CreateDbContext();
            await ctx.Database.ExecuteSqlAsync($"""
            INSERT INTO KeyValue(Key, Value) VALUES({ApplicationDbContext.AlignerAngle},{angle}) 
                ON CONFLICT(Key) DO UPDATE SET Value=excluded.Value
            """);
        }

        public async Task<Guid> Load(WaferTransferRequest req)
        {
            if (!await plcService.IsAutoMode())
            {
                throw new BadHttpRequestException("PLC not in auto mode!");
            }

            var presence = await plcService.TrackWafers();
            if (presence != 0)
            {
                throw new BadHttpRequestException("Wafer(s) left in the main chamber.");
            }

            using var ctx = dbFactory.CreateDbContext();
            var any = await ctx.Substrates
                .Where(s => s.Location != WaferLocation.Foup)
                .AnyAsync();
            if (any)
            {
                throw new BadHttpRequestException(WafersAtLargeMessage);
            }

            var state = await ctx.Substrates
                .Where(s => s.Foup == req.Foup && s.Slot == req.Slot)
                .Select(s => s.State)
                .FirstOrDefaultAsync();
            if (state != WaferState.Present && state != WaferState.Finished)
            {
                throw new BadHttpRequestException("Selected wafer is not present or has error");
            }

            var guid = await Lock(ctx);
            var eventStream = new EventStream(guid, substrateTrackingService);
            var angle = await GetAlignerAngle(ctx);
            _ = StartTransfer(LoadWafer(req, angle, eventStream));
            return guid;
        }

        public async Task<Guid> Unload(WaferTransferRequest req)
        {
            if (!await plcService.IsAutoMode())
            {
                throw new BadHttpRequestException("PLC not in auto mode!");
            }

            var presence = await plcService.TrackWafers();
            if (presence != 1)
            {
                throw new BadHttpRequestException("Wafer(s) left in the main chamber.");
            }

            using var ctx = dbFactory.CreateDbContext();
            var substrates = await ctx.Substrates
                .Where(s => s.Location != WaferLocation.Foup)
                .ToListAsync();
            if (substrates.Count != 1)
            {
                throw new BadHttpRequestException(WafersAtLargeMessage);
            }

            if (substrates[0].Location != WaferLocation.Stage)
            {
                throw new BadHttpRequestException("Substrates tracking is out of sync");
            }

            var guid = await Lock(ctx);
            var eventStream = new EventStream(guid, substrateTrackingService);
            _ = StartTransfer(UnloadWafer(req, eventStream));
            return guid;
        }

        public async Task<Guid> LoadAll(WaferLoadingQueueRequest request)
        {
            if (!await plcService.IsAutoMode())
            {
                throw new BadHttpRequestException("PLC not in auto mode!");
            }

            using var ctx = dbFactory.CreateDbContext();
            if (await plcService.ExistWafer())
            {
                throw new BadHttpRequestException("Wafer(s) left in the main chamber.");
            }

            var substrates = await ctx.Substrates.ToDictionaryAsync(s => (s.Foup, s.Slot));
            foreach (var s in substrates.Values)
            {
                if (s.Location != WaferLocation.Foup)
                {
                    throw new BadHttpRequestException(WafersAtLargeMessage);
                }
            }

            var slots = new List<(int, int)>();
            foreach (var slot in request.Slots)
            {
                if (!substrates.TryGetValue((request.Foup, slot), out var s) ||
                    s.State != WaferState.Present && s.State != WaferState.Finished)
                {
                    throw new BadHttpRequestException($"Wafer not present at Foup{request.Foup} Slot{slot}");
                }

                slots.Add((request.Foup, slot));
            }

            var guid = await Lock(ctx);
            var eventStream = new EventStream(guid, substrateTrackingService);
            var queue = new WaferLoadingQueue(efemService,
                plcService,
                motionControlService,
                hvClient,
                eChuckServiceClient,
                eventStream,
                shim,
                loggerFactory.CreateLogger<WaferLoadingQueue>())
            {
                AlignerAngle = await GetAlignerAngle(ctx),
                UseEChuck = request.UseEChuck,
                UseXYStage = request.UseXYStage
            };
            lock (lockObj)
            {
                loadingQueue = queue;
            }
            _ = StartTransfer(queue.LoadAll(slots));
            return guid;
        }

        public async Task<WaferLoadingQueue> GetLoadingQueue(Guid guid)
        {
            WaferLoadingQueue? queue;
            lock (lockObj)
            {
                queue = loadingQueue;
                if (queue == null)
                {
                    throw new BadHttpRequestException($"queue {guid} not found", StatusCodes.Status404NotFound);
                }
            }

            var v = guid.ToString();
            using var ctx = dbFactory.CreateDbContext();
            var present = await ctx.KeyValues
                .Where(kv => kv.Key == ApplicationDbContext.TransferId && kv.Value == v)
                .AnyAsync();
            if (!present)
            {
                throw new BadHttpRequestException($"queue {guid} not found", StatusCodes.Status404NotFound);
            }

            return queue;
        }

        async Task LoadWafer(WaferTransferRequest req, double angle, IEventStream eventStream)
        {
            await eventStream.StartTransfer();

            var (foup, slot, arm, useEChuck, useXYStage) = req;
            await using var scope = eventStream.NewScope(new Substrate
            {
                Foup = foup,
                Slot = slot,
                Location = WaferLocation.Foup
            });
            if (useXYStage)
            {
                await motionControlService.SwitchToEncoderPosition();
                await motionControlService.GoToLoadingPos();
            }

            shim.Start();
            var timeout = new AsyncTimeout();
            await timeout.Execute(TimeSpan.FromSeconds(30), plcService.OpenLoadLockDoor);

            logger.LogInformation("Picking up the wafer from foup {foup} slot {slot}.", slot, foup);
            await efemService.Load(Parameter.LoadPort(foup, slot), Parameter.Arm(arm));
            await scope.MoveTo(arm == 1 ? WaferLocation.Arm1 : WaferLocation.Arm2, WaferState.Loading);

            logger.LogInformation("Placing the wafer to aligner.");
            await efemService.UnLoad(Parameter.Aligner(), Parameter.Arm(arm));
            await scope.MoveTo(WaferLocation.Aligner);

            logger.LogInformation("Aligning...");
            await efemService.Align(Parameter.Angle(angle));
            logger.LogInformation("Aligning done.");

            logger.LogInformation("Picking up the wafer from aligner.");
            await efemService.Load(Parameter.Aligner(), Parameter.Arm(arm));
            await scope.MoveTo(arm == 1 ? WaferLocation.Arm1 : WaferLocation.Arm2);

            logger.LogInformation("Placing the wafer to loadlock.");
            await plcService.PutWaferRequest();
            await efemService.UnLoad(Parameter.LoadLock(), Parameter.Arm(arm));
            await scope.MoveTo(WaferLocation.LoadLock);

            await plcService.SetLastWafer(true);

            logger.LogInformation("Transferring the wafer to the main chamber.");
            //lifter pin到下位
            await timeout.Execute(TimeSpan.FromSeconds(60), plcService.PutWaferEnd);
            logger.LogInformation("Lifter pin down");

            await scope.MoveTo(WaferLocation.Stage);

            if (useEChuck)
            {
                await eChuckServiceClient.PutWaferValue();
                await eChuckServiceClient.AdsorptionAction();
                await eChuckServiceClient.AdsorptionCheck();
                logger.LogInformation("EChuck Chuck wafer done.");
                await eChuckServiceClient.PunctureAction();
                logger.LogInformation("EChuck Puncture wafer done.");
            }

            if (useXYStage)
            {
                await motionControlService.SetAbsolutePosition(155, 205);
                await motionControlService.SwitchToLaserPosition();
                logger.LogInformation("Motion Switch to Laser Mode done.");
            }

            await scope.SetState(WaferState.Processing);
            scope.Complete();
            await eventStream.EndTransfer();
        }

        async Task UnloadWafer(WaferTransferRequest req, IEventStream eventStream)
        {
            await eventStream.StartTransfer();

            var (foup, slot, arm, useEChuck, useXYStage) = req;
            await using var scope = eventStream.NewScope(new Substrate
            {
                Foup = foup,
                Slot = slot,
                Location = WaferLocation.Stage
            });
            await scope.SetState(WaferState.Unloading);

            if (useXYStage)
            {
                await hvClient.StageHVOFF();

                await motionControlService.SwitchToEncoderPosition();
                logger.LogInformation("Servo Mode Switch to Encoder Mode end!");
                await motionControlService.GoToLoadingPos();
            }

            if (useEChuck)
            {
                await eChuckServiceClient.ReleaseAction();
            }

            shim.Start();

            await plcService.SetWaferDetectionFinished();
            await scope.MoveTo(WaferLocation.LoadLock);

            await AsyncTimeout.Run(TimeSpan.FromSeconds(60), plcService.GetWaferStart);

            logger.LogInformation("Picking up the wafer from loadlock.");
            await efemService.Load(Parameter.LoadLock(), Parameter.Arm(arm));
            await plcService.GetWaferEnd();
            await scope.MoveTo(arm == 1 ? WaferLocation.Arm1 : WaferLocation.Arm2);

            logger.LogInformation("Placing the wafer to foup {foup} slot {slot}.", slot, foup);
            await efemService.UnLoad(Parameter.LoadPort(foup, slot), Parameter.Arm(arm));
            await scope.MoveTo(WaferLocation.Foup, WaferState.Finished);

            scope.Complete();
            await eventStream.EndTransfer();
        }

        async Task StartTransfer(Task transfer)
        {
            try
            {
                await transfer;
                await Unlock();
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "transfer failed");
            }
            finally
            {
                shim.Stop();
                Interlocked.Exchange(ref busy, 0);
                lock (lockObj)
                {
                    loadingQueue = null;
                }
            }
        }

        async Task<double> GetAlignerAngle(ApplicationDbContext ctx)
        {
            var v = await ctx.KeyValues
                .Where(kv => kv.Key == ApplicationDbContext.AlignerAngle)
                .Select(kv => kv.Value)
                .FirstOrDefaultAsync();
            return v == null ? efemOptions.Value.AlignerAngle : double.Parse(v);
        }

        async Task<Guid> Lock(ApplicationDbContext ctx)
        {
            if (Interlocked.CompareExchange(ref busy, 1, 0) != 0)
            {
                throw new BadHttpRequestException("Another transfer is in progress.");
            }

            var guid = Guid.NewGuid();
            try
            {
                ctx.KeyValues.Add(new KeyValue
                {
                    Key = ApplicationDbContext.TransferId,
                    Value = guid.ToString()
                });
                await ctx.SaveChangesAsync();
                return guid;
            }
            catch (Exception ex)
            {
                Interlocked.Exchange(ref busy, 0);
                throw new BadHttpRequestException("Another transfer is in progress.", ex);
            }
        }

        async Task Unlock()
        {
            using var ctx = dbFactory.CreateDbContext();
            await ctx.KeyValues
                .Where(kv => kv.Key == ApplicationDbContext.TransferId)
                .ExecuteDeleteAsync();
        }
    }
}
